﻿using System;
using System.Collections.Generic;
using System.ComponentModel.Design;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Runtime.InteropServices;

using EnvDTE;

using EnvDTE80;

using Microsoft.VisualStudio.Shell;

namespace VsExt.CollapseSolution
{
    ///<summary>
    ///  This is the class that implements the package exposed by this assembly. The minimum requirement for a class to be considered a valid package for Visual Studio is to implement the IVsPackage interface and register itself with the shell. This package uses the helper classes defined inside the Managed Package Framework (MPF) to do it: it derives from the Package class that provides the implementation of the IVsPackage interface and uses the registration attributes defined in the framework to register itself and its components with the shell.
    ///</summary>
    // This attribute tells the PkgDef creation utility (CreatePkgDef.exe) that this class is
    // a package. GUID
    // This attribute is used to register the information needed to show the this package
    // in the Help/About dialog of Visual Studio.
    // This attribute is needed to let the shell know that this package exposes some menus.
    //[ProvideAutoLoad(Microsoft.VisualStudio.Shell.Interop.UIContextGuids80.SolutionExists)]
    [PackageRegistration(UseManagedResourcesOnly = true)]
    [InstalledProductRegistration("#110", "#112", "1.0", IconResourceID = 400)]
    [ProvideMenuResource("Menus.ctmenu", 1)]
    [Guid(GuidList.guidCollapseSolutionPkgString)]
    public sealed class VsExt_CollapseSolutionPackage : Package
    {
        private DTE2 Dte { get; set; }

        private UIHierarchy SolutionExplorer { get; set; }

        private MenuCommand SolutionExplorerToolbarCommand { get; set; }

        /// <summary>
        ///   Default constructor of the package. Inside this method you can place any initialization code that does not require any Visual Studio service because at this point the package object is created but not sited yet inside Visual Studio environment. The place to do all the other initialization is the Initialize method.
        /// </summary>
        public VsExt_CollapseSolutionPackage()
        {
            Trace.WriteLine(string.Format(CultureInfo.CurrentCulture, "constructor for: {0}", ToString()));
        }

        /// <summary>
        ///   Initialization of the package; this method is called right after the package is sited, so this is the place where you can put all the initialization code that rely on services provided by VisualStudio.
        /// </summary>
        protected override void Initialize()
        {
            Trace.WriteLine(string.Format(CultureInfo.CurrentCulture, "Entering Initialize() of: {0}", ToString()));

            base.Initialize();

            Dte = (DTE2)GetService(typeof(DTE));

            initializeMenuCommands();
        }

        /// <summary>
        /// Add our command handlers for menu
        /// </summary>
        /// <remarks>commands must exist in the .vsct file</remarks>
        private void initializeMenuCommands()
        {
            OleMenuCommandService mcs = GetService(typeof(IMenuCommandService)) as OleMenuCommandService;

            if (null == mcs)
            {
                return;
            }

            // Create the command for the menu item.
            CommandID menuCommandCollapseSolution = new CommandID(GuidList.guidCollapseSolutionCmdSet, (int)PkgCmdIDList.cmdidCollapseTree);
            MenuCommand menuItemSolution = new MenuCommand(menuItemCallback, menuCommandCollapseSolution);
            mcs.AddCommand(menuItemSolution);

            CommandID menuCommandCollapseProject = new CommandID(GuidList.guidCollapseProjectCmdSet, (int)PkgCmdIDList.cmdidCollapseProject);
            MenuCommand menuItemProject = new MenuCommand(menuItemCallback, menuCommandCollapseProject);
            mcs.AddCommand(menuItemProject);

            CommandID menuCommandCollapseProjectMulti = new CommandID(GuidList.guidCollapseSolutionCmdSetProjectMulti, (int)PkgCmdIDList.cmdidCollapseProjectMulti);
            MenuCommand menuItemProjectMulti = new MenuCommand(menuItemCallback, menuCommandCollapseProjectMulti);
            mcs.AddCommand(menuItemProjectMulti);

            CommandID menuCommandCollapseSolutionNoSelection = new CommandID(GuidList.guidCollapseSolutionNoSelectionCmdSet, (int)PkgCmdIDList.cmdidCollapseTreeNoSelection);
            MenuCommand menuItemNoSelection = new MenuCommand(menuItemCallbacNoSelection, menuCommandCollapseSolutionNoSelection);

            mcs.AddCommand(menuItemNoSelection);
        }

        /// <summary>
        ///   Collapses all items and their child items.
        /// </summary>
        private void collapseHierarchy()
        {
            object[] selection = getSelectedUiHierarchy(SolutionExplorer);

            if (selection == null || selection.Length == 0)
            {
                return;
            }

            Dte.SuppressUI = true;

            // return only the top level items.
            List<UIHierarchyItem> topLevel = getTopLevelUiHierarchyItems(selection);

            // collapse the top level items first
            foreach (UIHierarchyItem item in topLevel)
            {
                collapseHierarchyItem(item);
            }

            // now collapse the entire hierarchy for each top level node.
            foreach (UIHierarchyItem selected in topLevel.Where(selected => selected != null))
            {
                // collapse each item in the hierarchy.
                processHierarchy(selected);
            }

            Dte.SuppressUI = false;

            // Select the solution/project node or else when you click on the solution window
            // scrollbar, it will synchronize the open document with the tree and pop
            // out the corresponding node.
            SolutionExplorer.UIHierarchyItems.Item(1).Select(vsUISelectionType.vsUISelectionTypeSelect);
        }

        /// <summary>
        ///   Collapses the hierarchy item.
        /// </summary>
        /// <param name="item"> The item. </param>
        private void collapseHierarchyItem(UIHierarchyItem item)
        {
            // don't collapse a solution node because it would cause it to be re-drawn
            // and that takes a long time.
            if (!item.UIHierarchyItems.Expanded || (item.Object is Solution))
            {
                return;
            }

            item.UIHierarchyItems.Expanded = false;

            if (!item.UIHierarchyItems.Expanded)
            {
                return;
            }

            item.Select(vsUISelectionType.vsUISelectionTypeSelect);

            SolutionExplorer.DoDefaultAction();
        }

        /// <summary>
        ///   Gets the selected UI hierarchy.
        /// </summary>
        /// <returns> </returns>
        private static object[] getSelectedUiHierarchy(UIHierarchy solutionExplorer)
        {
            object[] selection = solutionExplorer.SelectedItems as object[];

            return selection;
        }

        /// <summary>
        ///   Identifies the top most hierarchy item within the selection.
        /// </summary>
        /// <param name="selection"> </param>
        /// <returns> </returns>
        private static List<UIHierarchyItem> getTopLevelUiHierarchyItems(IEnumerable<object> selection)
        {
            List<UIHierarchyItem> nodes = new List<UIHierarchyItem>();

            // collapse only the project level first
            // if the solution node was selected then we need to get the top-levels from the items underneath the solution node.

            foreach (UIHierarchyItem item in selection.OfType<UIHierarchyItem>())
            {
                if (item.Object is Solution)
                {
                    nodes.AddRange(from UIHierarchyItem solutionItem in item.UIHierarchyItems
                                   where solutionItem.Object is Project || solutionItem.Object is SolutionFolder
                                   select solutionItem);
                }
                else if (item.Object is Project || item.Object is SolutionFolder)
                {
                    nodes.Add(item);
                }
            }

            return nodes;
        }

        /// <summary>
        ///   Determines if the SolutionExplorer tool windows has at least one hierarchy item.
        /// </summary>
        /// <returns> </returns>
        private bool isCommandExecutable()
        {
            bool executable = false;

            try
            {
                // Get the the Solution Explorer tree
                SolutionExplorer = Dte.ToolWindows.SolutionExplorer;

                // Check if there is any open solution
                if ((SolutionExplorer.UIHierarchyItems.Count > 0))
                {
                    executable = true;
                }
            }
            catch (Exception)
            {
                executable = false;
            }

            return executable;
        }

        /// <summary>
        ///   This function is the callback used to execute a command when the a menu item is clicked. See the Initialize method to see how the menu item is associated to this function using the OleMenuCommandService service and the MenuCommand class.
        /// </summary>
        private void menuItemCallback(object sender, EventArgs e)
        {
            if (!isCommandExecutable())
            {
                return;
            }

            collapseHierarchy();
        }

        /// <summary>
        ///   Select the first hierarchy item in the solution explorer windows and
        /// </summary>
        /// <param name="sender"> </param>
        /// <param name="e"> </param>
        private void menuItemCallbacNoSelection(object sender, EventArgs e)
        {
            if (!isCommandExecutable())
            {
                return;
            }

            // select the solution node
            SolutionExplorer.UIHierarchyItems.Item(1).Select(vsUISelectionType.vsUISelectionTypeSelect);

            collapseHierarchy();
        }

        /// <summary>
        ///   Processes the hierarchy.
        /// </summary>
        /// <param name="item"> The item. </param>
        private void processHierarchy(UIHierarchyItem item)
        {
            // now process each of the child nodes first
            foreach (UIHierarchyItem nestedItem in item.UIHierarchyItems)
            {
                processHierarchy(nestedItem);
            }

            collapseHierarchyItem(item);
        }
    }
}